#include <cmath>
#include "app/ApplicationConfig.h"
#include "GUIEngine.h"
#include "util/exceptions/ElementNotFoundException.h"
#include "util/MathUtils.h"

namespace gui {

GUIEngine::GUIEngine(int width, int height, int bpp, std::string caption,
		bool fullScreen, int deltaX, int deltaY) {
	this->screenRect.w = width;
	this->screenRect.h = height;
	this->screenRect.x = 0;
	this->screenRect.y = 0;
	this->mapRect.x = -deltaX;
	this->mapRect.y = -deltaY;

	this->screenBPP = bpp;
	this->screenCaption = caption;
	this->fullScreen = fullScreen;
	this->imageCache = new ImageCache();
	this->xScale = 1;
	this->yScale = 1;
}

GUIEngine::~GUIEngine() {
	delete this->imageCache;
}

bool GUIEngine::start(int h, int w, bool splashScreen) {
	if (SDL_Init(SDL_INIT_VIDEO) == -1) {
		return false;
	}

	int defaults = SDL_HWSURFACE | SDL_DOUBLEBUF;

	this->screen = SDL_SetVideoMode(this->screenRect.w, this->screenRect.h, this->screenBPP,
			(this->fullScreen) ? defaults | SDL_FULLSCREEN : defaults);

	if (this->screen == NULL) {
		return false;
	}
    this->rColor = app::ApplicationConfig::getOptionalIntProperty("bgColorR", 242);
    this->gColor = app::ApplicationConfig::getOptionalIntProperty("bgColorG", 239);
    this->bColor = app::ApplicationConfig::getOptionalIntProperty("bgColorB", 233);
    this->fillSurface(this->screen, this->rColor, this->gColor, this->bColor);

    if (splashScreen) this->showSplash();

    this->mapSurface = SDL_CreateRGBSurface(SDL_HWSURFACE, w, h, this->screenBPP, 0, 0, 0, 0);
    this->fillSurface(this->mapSurface, this->rColor, this->gColor, this->bColor);

    SDL_WM_SetCaption(this->screenCaption.c_str(), NULL);
	return true;
}

void GUIEngine::setScale(double xScale, double yScale) {
    this->xScale = xScale;
    this->yScale = yScale;
}

void GUIEngine::stop() {
	SDL_FreeSurface(this->screen);
	SDL_Quit();
}

void GUIEngine::drawGUIObjects(util::LinkedList<GUIObject*>* guiObjects, bool clear) {
    SDL_Surface* aux;
	SDL_Surface* surface;
	SDL_Rect position;

	// se dibujan de a uno los GUIObjects dentro de this->mapSurface
	util::LinkedList<GUIObject*>::Iterator* it = guiObjects->iterator();
	while (it->hasNext()) {

		// obtengo el GUIObject actual y sus posiciones
		GUIObject* currentObject = it->next();

		// si tiene una textura (filename) seteada
		if (currentObject->getTexture().compare("") != 0) {

			// dibujo la imagen sobre la superficie "aux"
            SDL_Surface* sprite = this->imageCache->getImage(currentObject->getTexture());
			int height = (currentObject->getHeight() != 0) ? currentObject->getHeight() : sprite->h;

			if (currentObject->getTextureNoRepeat()){
				aux = this->copySurface(sprite, sprite->h, sprite->w);
			} else {
				SDL_Surface* aux2 = this->resize(sprite, 1, height);
				aux = this->copySurface(aux2, aux2->h, aux2->w);
				SDL_FreeSurface(aux2);
			}

        } else {
        	// si no contiene una imagen, dibujo sobre aux algo generico
            aux = SDL_CreateRGBSurface(SDL_HWSURFACE,
            		currentObject->getWidth(),
            		currentObject->getHeight(), this->screenBPP,
            		currentObject->getRColor(),
            		currentObject->getGColor(),
            		currentObject->getBColor(), 0);
        }

		// roto la imagen del GUIObject (si tiene rotacion 0 no hace nada)
		// y la guardo en "surface"
		surface = this->fineRotation(aux, currentObject->getRotation());
		SDL_FreeSurface(aux);

		if (currentObject->getTextureHasTransparentColor()){
			SDL_SetColorKey(surface, SDL_SRCCOLORKEY, SDL_MapRGB(surface->format,
									currentObject->getTextureTransparentColorR(),
									currentObject->getTextureTransparentColorG(),
									currentObject->getTextureTransparentColorB()));
		}
		position.x = (currentObject->getXPos() - (surface->w / 2)) + mapRect.x;
		position.y = (currentObject->getYPos() - (surface->h / 2)) + mapRect.y;
		// dibujo la imagen del GUIObject, cargada y rotada dentro de mapSurface
		SDL_BlitSurface(surface, NULL, this->mapSurface, &position);
		SDL_FreeSurface(surface);
	}
	delete it;

	// una vez que esta toodo graficado sobre mapSurface, dibujo este mapa
	// en el screen con drawSurface (dibuja en screen y refresca la pantalla)
	// con la escala apropiada
	SDL_Surface* scaledMap = this->applyScale(this->mapSurface);
	SDL_FreeSurface(this->mapSurface);
	this->mapSurface = scaledMap;
	this->drawSurface(this->mapSurface, NULL);
}

void GUIEngine::clearScreen() {
	this->fillSurface(this->screen, this->rColor, this->gColor, this->bColor);
}

int GUIEngine::pollEvent(GUIEvent* event) {
	SDL_Event sdlEvent;
	int ret = SDL_PollEvent(&sdlEvent);
	if (ret) {
		event->fillEvent(sdlEvent);
	}
	return ret;
}

int GUIEngine::waitEvent(GUIEvent* event) {
	SDL_Event sdlEvent;
	int ret = SDL_WaitEvent(&sdlEvent);
	event->fillEvent(sdlEvent);
	return ret;
}

bool GUIEngine::getKeyState(GUIEventKey key) {
	Uint8* keyState = SDL_GetKeyState(NULL);
	return (bool) keyState[key];
}


SDL_Surface* GUIEngine::loadImage(std::string fileName, int transparentColorR,
		int transparentColorG, int transparentColorB) {
	SDL_Surface* aux = NULL;
	SDL_Surface* ret = NULL;
	aux = SDL_LoadBMP(fileName.c_str());
	if (aux != NULL) {
		ret = SDL_DisplayFormat(aux);
		SDL_FreeSurface(aux);
	}
	SDL_SetColorKey(ret, SDL_RLEACCEL | SDL_SRCCOLORKEY, SDL_MapRGB(ret->format, transparentColorR, transparentColorG, transparentColorB));
	return ret;
}

Uint32 GUIEngine::getPixel(SDL_Surface* surface, int x, int y) {
	Uint32* pixels = (Uint32*) surface->pixels;
	return pixels[(surface->w * y) + x];
}

void GUIEngine::putPixel(SDL_Surface* surface, int x, int y, Uint32 pixel) {
	Uint32* pixels = (Uint32*) surface->pixels;
	pixels[(surface->w * y) + x] = pixel;
}

SDL_Surface* GUIEngine::resize(SDL_Surface* surface, double x, double y) {
	if (x < 0 || y < 0) {
		return this->copySurface(surface, surface->h, surface->w);
	}
	int newWidth = (util::doubleToInt(surface->w * x) >= 1) ? util::doubleToInt(surface->w * x) : 1;
	int newHeight = (util::doubleToInt(surface->h * y) >= 1) ? util::doubleToInt(surface->h * y) : 1;
	int extraByteX = 0;
	int extraByteY = 0;
	x = newWidth / (double)surface->w;
	y = newHeight / (double)surface->h;
	int x_int = (int)x;
	int y_int = (int)y;
	double remainderX = x - x_int;
	double remainderY = y - y_int;
	double acumuladoX = 0.0;
	double acumuladoY = 0.0;
	SDL_Surface* stretchedSurface = this->createSurface(surface, newHeight, newWidth);
	this->lockSurface(surface);
	int i_t = 0;
	for (int i = 0; i < surface->w; i++) {
		acumuladoX += remainderX;
		extraByteX = acumuladoX >= 1.0? 1 : 0;
		acumuladoX -= extraByteX;
		for(int k = 0; k < (x_int + extraByteX); k++ ) {
			acumuladoY = 0.0;
			int j_t = 0;
			acumuladoY = 0.0;
			for (int j = 0; j < surface->h; j++) {
				Uint32 pixel = this->getPixel(surface, i, j);
				acumuladoY += remainderY;
				extraByteY = (acumuladoY >= 1.0)? 1 : 0;
				acumuladoY -= extraByteY;
				for (int l = 0; l < (y_int + extraByteY); l++) {
					this->putPixel(stretchedSurface, i_t + k, j_t + l, pixel);
				}
				j_t+= y_int + extraByteY;
			}
		}
		i_t+= x_int + extraByteX;
	}
	this->unlockSurface(surface);
	this->handleKeyColor(surface, stretchedSurface);
	return stretchedSurface;
}

SDL_Surface* GUIEngine::zoom(SDL_Surface* surface, double z) {
	return resize(surface, z, z);
}

SDL_Surface* GUIEngine::rotate180Degrees(SDL_Surface* surface) {
	SDL_Surface* rotatedSurface = this->copySurface(surface, surface->h,
			surface->w);
	this->lockSurface(surface);
	for (int i = 0, ri = surface->w - 1; i < surface->w; i++, ri--) {
		for (int j = 0, rj = surface->h - 1; j < surface->h; j++, rj--) {
			Uint32 pixel = this->getPixel(surface, i, j);
			this->putPixel(rotatedSurface, ri, rj, pixel);
		}
	}
	this->unlockSurface(surface);
	this->handleKeyColor(surface, rotatedSurface);
	return rotatedSurface;
}

SDL_Surface* GUIEngine::rotate90Degrees(SDL_Surface* surface, bool clockwise) {
	SDL_Surface* rotatedSurface = this->copySurface(surface, surface->w,
			surface->h);
	this->lockSurface(surface);
	for (int i = 0, ri = surface->w - 1; i < surface->w; i++, ri--) {
		for (int j = 0, rj = surface->h - 1; j < surface->h; j++, rj--) {
			Uint32 pixel = this->getPixel(surface, i, j);
			if (clockwise) {
				this->putPixel(rotatedSurface, rj, i, pixel);
			} else {
				this->putPixel(rotatedSurface, j, ri, pixel);
			}
		}
	}
	this->unlockSurface(surface);
	this->handleKeyColor(surface, rotatedSurface);
	return rotatedSurface;
}

SDL_Surface* GUIEngine::rotateSmallAngle(SDL_Surface* surface, double angle) {
	double cosAngle = cos(angle);
	double sinAngle = sin(angle);
	int newH = util::doubleToInt(cosAngle * surface->h + sinAngle * surface->w);
	int newW = util::doubleToInt(cosAngle * surface->w + sinAngle * surface->h);

	SDL_Surface* ret = this->copySurface(surface, newH, newW);
	// Color de transparencia 0000FF.
	Uint32 transparentColor = (surface->flags & SDL_SRCCOLORKEY) ? surface->format->colorkey
					: SDL_MapRGB(ret->format, 0, 0, 255);
	this->fillSurface(ret, transparentColor);
	this->lockSurface(surface);
	int dispX = util::doubleToInt(surface->h * sinAngle);
	for (int i = 0; i < surface->w; i++) {
		for (int j = 0; j < surface->h; j++) {
			Uint32 pixel = this->getPixel(surface, i, j);
			int a = util::doubleToInt((i * cosAngle) - (j * sinAngle)) + dispX;
			int b = util::doubleToInt((i * sinAngle) + (j * cosAngle));
			this->putPixel(ret, a, b, pixel);
			if (a > 0 && a < ret->w) {
				// Workaround para huecos al rotar.
				this->putPixel(ret, a + 1, b, pixel);
			}
		}
	}
	this->unlockSurface(surface);
	SDL_SetColorKey(ret, SDL_RLEACCEL | SDL_SRCCOLORKEY, transparentColor);
	return ret;
}

SDL_Surface* GUIEngine::fineRotation(SDL_Surface* surface, double angle) {
	angle = util::doubleLess(angle, 0, false) ? 2 * PI + angle : angle;
	if ((util::doubleEquals(angle,0)) || (util::doubleEquals(angle, 2 * PI))) {
		return this->copySurface(surface, surface->h, surface->w);
	} else if (util::doubleIsBetween(angle, 0, PI / 2.0, false)) {
		return this->rotateSmallAngle(surface, angle);
	} else if (util::doubleEquals(angle, PI / 2.0)) {
		return this->rotate90Degrees(surface, true);
	} else if (util::doubleIsBetween(angle, PI / 2.0, PI, false)) {
		SDL_Surface* aux = this->rotate90Degrees(surface, true);
		SDL_Surface* aux2 = this->rotateSmallAngle(aux, angle - PI / 2.0);
		SDL_FreeSurface(aux);
		return aux2;
	} else if (util::doubleEquals(angle, PI)) {
		return this->rotate180Degrees(surface);
	} else if (util::doubleIsBetween(angle, PI, 3 * PI / 2.0, false)) {
		SDL_Surface* aux = this->rotate180Degrees(surface);
		SDL_Surface* aux2 = this->rotateSmallAngle(aux, angle - PI);
		SDL_FreeSurface(aux);
		return aux2;
	} else if (util::doubleEquals(angle, 3 * PI / 2.0)) {
		return this->rotate90Degrees(surface, false);
	} else if (util::doubleIsBetween(angle, 3 * PI / 2.0, 2 * PI, false)) {
		SDL_Surface* aux =  this->rotate90Degrees(surface, true);
		SDL_Surface* aux2 = this->rotate180Degrees(aux);
		SDL_Surface* aux3 = this->rotateSmallAngle(aux2, angle - 3 * PI / 2.0);
		SDL_FreeSurface(aux);
		SDL_FreeSurface(aux2);
		return aux3;
	} else {
		return this->fineRotation(surface, angle - 2 * PI);
	}
}

SDL_Rect GUIEngine::getRectFromPortion(Portion portion) {
	SDL_Rect ret;
	ret.x = portion.x;
	ret.y = portion.y;
	ret.h = portion.h;
	ret.w = portion.w;
	return ret;
}

void GUIEngine::refreshScreen() {
	SDL_Flip(this->screen);
}

SDL_Surface* GUIEngine::createSurface(GUIObject* object) {
	SDL_Surface* ret;
	if (object->getTexture().compare("") != 0) {
		SDL_Surface* ret2 = this->loadImage(object->getTexture(),
				object->getTextureTransparentColorR(),
				object->getTextureTransparentColorG(),
				object->getTextureTransparentColorB());
		//SDL_Surface* ret2 = this->imageCache->getImage(object->getTexture());
		ret = this->fineRotation(ret2, object->getRotation());
		SDL_FreeSurface(ret2);
	} else {
		ret = SDL_CreateRGBSurface(SDL_HWSURFACE, object->getWidth(),
				object->getHeight(), this->screenBPP, 0, 0, 0, 0);
		// this->fillSurface(ret, object->getRColor(), object->getGColor(), object->getBColor());
	}
	return ret;
}

void GUIEngine::drawSurface(SDL_Surface* surface, SDL_Rect* position) {
	SDL_BlitSurface(surface, NULL, this->screen, position);
	this->refreshScreen();
}

SDL_Surface* GUIEngine::copySurface(SDL_Surface* surface, int h, int w) {
	SDL_Surface* ret = createSurface(surface, h, w);
	SDL_BlitSurface(surface, NULL, ret, NULL);
	return ret;
}

SDL_Surface* GUIEngine::createSurface(SDL_Surface* surface, int h, int w) {
	SDL_Surface* ret = NULL;
	ret = SDL_CreateRGBSurface(SDL_HWSURFACE, w, h,
				surface->format->BitsPerPixel, surface->format->Rmask,
				surface->format->Gmask, surface->format->Bmask, surface->format->Amask);
	return ret;
}

void GUIEngine::fillSurface(SDL_Surface* surface, int r, int g, int b) {
	this->fillSurface(surface, SDL_MapRGB(surface->format, r, g, b));
}

void GUIEngine::fillSurface(SDL_Surface* surface, Uint32 color) {
	SDL_Rect rect;
	rect.x = 0;
	rect.y = 0;
	rect.h = surface->h;
	rect.w = surface->w;
	SDL_FillRect(surface, &rect, color);
}

void GUIEngine::lockSurface(SDL_Surface* surface) {
	if (SDL_MUSTLOCK(surface)) {
		SDL_LockSurface(surface);
	}
}

void GUIEngine::unlockSurface(SDL_Surface* surface) {
	if (SDL_MUSTLOCK(surface)) {
		SDL_UnlockSurface(surface);
	}
}

void GUIEngine::handleKeyColor(SDL_Surface* originalSurface,
		SDL_Surface* newSurface) {
	if (originalSurface->flags & SDL_SRCCOLORKEY) {
		SDL_SetColorKey(newSurface, SDL_RLEACCEL | SDL_SRCCOLORKEY,
				originalSurface->format->colorkey);
	}
}

void GUIEngine::showSplash() {
	SDL_Surface* splash = SDL_LoadBMP("res/img/splash.bmp");
	SDL_SetColorKey(splash, SDL_RLEACCEL | SDL_SRCCOLORKEY, SDL_MapRGB(splash->format, 255, 255, 255));
	SDL_Rect pos;
	pos.x = util::doubleToInt((this->screen->w - splash->w) / 2.0);
	pos.y = util::doubleToInt((this->screen->h - splash->h) / 2.0);
	this->drawSurface(splash, &pos);
	SDL_FreeSurface(splash);
}

void GUIEngine::resizePortion(Portion* portion){
	portion->w = util::doubleToInt(portion->w * this->xScale);
	portion->x = util::doubleToInt(portion->x * this->xScale);
	portion->h = util::doubleToInt(portion->h * this->yScale);
	portion->y = util::doubleToInt(portion->y * this->yScale);

}

void GUIEngine::drawPortion(Portion portion) {
   // transformo la dimension del portion segun la escala en la que se muestra la misma
   this->resizePortion(&portion);
   SDL_Rect rect = this->getRectFromPortion(portion);
   SDL_Surface* aux = this->copySurface(this->mapSurface, portion.h, portion.w);
   // TODO: Remplazar harcode
   // SDL_Surface* aux = this->copySurface(this->mapSurface, this->mapSurface->h, this->mapSurface->w);
   this->fillSurface(aux, this->rColor, this->gColor, this->bColor);
   // SDL_BlitSurface(this->mapSurface, &rect, aux, &rect);
   SDL_BlitSurface(this->mapSurface, NULL, aux, &rect);
   SDL_SetColorKey(aux, SDL_RLEACCEL | SDL_SRCCOLORKEY, this->mapSurface->format->colorkey);
   SDL_Rect rectAux;
   rectAux.x = -rect.x;
   rectAux.y = -rect.y;
   this->clearScreen();
   this->drawSurface(aux, &rectAux);
   SDL_FreeSurface(aux);
}

SDL_Surface* GUIEngine::applyScale(SDL_Surface* surface) {
	return this->resize(surface, this->xScale, this->yScale);
}

}
